Skip to content

feat(zeo): validate/generate START parameters from a per-device capability table - #967

Open
NOisi-x wants to merge 26 commits into
Python-roborock:mainfrom
NOisi-x:pr/zeo-program-config
Open

NOisi-x wants to merge 26 commits into
Python-roborock:mainfrom
NOisi-x:pr/zeo-program-config

Conversation

@NOisi-x

@NOisi-x NOisi-x commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Build on #897

Problem
start_with sends mode, program and several wash/dry parameters together, but
not every combination is valid. Each (mode, program) accepts only a specific
set of levels, and the level index is not the protocol enum value —
drying_mode is a cross-mapping (Low→Iron, Mid→Quick, High→Store), and
soak/steam levels are offset. Callers had no way to know what a
device/programme accepts and sent blindly.

This PR
Adds a per-device capability table (roborock/data/zeo/) that

  1. resolves the mode templates a device supports from model/region/feature
    bits (resolve_mode_templates / resolve_data_module / build_mode_templates);
  2. validates (validate_start_params) or generates (default_start_params) a
    valid START parameter set for a (mode, program), using that device's own
    data module. Covers 70 modules / 4212 mode records.

Follow-up
The exposed level data (ZeoParamConfig.support / raw_values, ZeoTimeOptions,
ZeoModeLists) makes it possible to offer only valid choices in a frontend, so
users cannot pick an illegal combination. It would be great if the integration
maintainers could take this further and implement it end to end.

NOisi-X and others added 26 commits July 20, 2026 11:39
Add MqttQos enum (AT_MOST_ONCE=0, AT_LEAST_ONCE=1, EXACTLY_ONCE=2) and thread a qos parameter through the publish chain (MqttSession -> MqttChannel -> send_decoded_command). All existing callers keep default AT_MOST_ONCE (backward compatible). Also add a unix timestamp field to A01 encode_mqtt_payload, required by Zeo/Dyad devices for command acceptance.
… all 56 devices covered

Expand RoborockZeoProtocol from 31 to 67 DP entries, ordered by numeric ID. Add all missing enum classes (ZeoFeatureBits, ZeoDryingMethod, ZeoSteamVolume, ZeoDryAndCare, ZeoDryerStartError) and extend existing enums to cover every state/value found in the official app plugin bundle. Add ZeoStartParams, ZeoCustomMode, and ZeoDryerCustomMode data containers inheriting from RoborockBase, placed in zeo_containers.py per reviewer guidance.
Update ZeoStartParams, ZeoCustomMode, and ZeoDryerCustomMode to use typed enum fields (ZeoMode, ZeoProgram, ZeoTemperature, etc.) instead of raw int, aligning with the V1 container pattern in v1_containers.py. Rename shorthand fields (rinse_times→rinse, spin_level→spin) for consistency across all three classes. Unify drying-mode field naming.
…overy

Subscribes to the device DPS MQTT topic after connection. Incoming RPC_RESPONSE messages are decoded and merged into _dps_cache with incremental updates. _discover_features() queries FEATURE_BITS (DP 237) to wake the device and cache capabilities — equivalent to V1's discover_features(). Also fixes TraitUpdateListener init in ZeoApi and a01_properties routing in connect().
except Exception → except RoborockException (aligns with Bundle's silent fallback to 0)
try/except only wraps decode_rpc_response — cache updates and notify must propagate
…e's forceLoad()

Sends ID_QUERY with 28 base DPs (including FEATURE_BITS) in a single round-trip after MQTT subscribe. This triggers a complete state dump from the device 鈥?matching Bundle's startup flow exactly. For devices known to lack FEATURE_BITS (a63, a90), the DP is excluded from the query list.
Address reviewer feedback: integrate ZeoApi.close() into RoborockDevice.close()
and implement the second-stage feature DP load matching Bundle's
loadFeatureDps(). The first force-load now also includes smart-hosting DPs
(235/236/238), and a follow-up query fetches feature-gated DPs (silent mode,
dry care, smile light, dirt detection, wash/dry linkage, etc.) plus UV light
gated by a series whitelist. Feature-load failures are non-fatal.
VoiceVolume (10009) and VoiceSwitch (10301) are read-write: they have
getters (voiceVolume / isVoiceSwitchOn) and appear in the bundle's
loadFeatureDps query list (gated by FeatureBit.VoiceAssistant). Add
their _try_json converters to the read-write protocol entries so
query_values() returns parsed JSON.

SetSoundPackage (10003) and VoiceRecordDelete (10304) are write-only
(no getter, not in any query list) - remove their dead converters.

Rename module-level __init__ to __all__ (former was a bug that
overshadowed the module's __init__ attribute).
_settings/_status are lazily constructed; _update_settings_from_dps only
routed pushes into a trait after it was built. State pushed before the
first access landed in _dps_cache but was never applied, so the first
access returned a fresh empty trait and silently lost all prior state.

Treat _dps_cache as the single source of truth: on first construction,
each trait backfills itself via update_from_dps(self._dps_cache).
update_from_dps ignores undeclared DPs, so feeding the whole cache is safe.
Resolve conflicts in the A01 traits after upstream Python-roborock#941 (track A01 device
state from push updates) and Python-roborock#946 (skip DP 225 on Zeo H1):

- Adopt the upstream A01Api base class and its �alues state tracking.
- Build the typed Zeo traits (command/settings/status) eagerly instead of
  lazily, and drop _dps_cache plus the first-access backfill entirely.
- Add an A01Api._merge_datapoints() hook so raw datapoints are projected
  into the traits and merged into �alues from a single path: unsolicited
  pushes, the initial cloud status snapshot, and (when not subscribed) query
  responses.
- Keep the upstream supports() helper, and derive eatures from
  _feature_bits as before.
- Keep upstream's supports_default_setting() (H1 lacks DP 225).
decode_rpc_response() keys the response by integer DP code, while the send_decoded_command() overload reports RoborockZeoProtocol keys. mypy flagged the mismatch when the response was passed to _update_traits(), which expects dict[int, Any]. Normalize the keys explicitly so the runtime representation and the declared type agree.
…ility table

Problem
start_with sends mode, program and several wash/dry parameters together, but
not every combination is valid. Each (mode, program) accepts only a specific
set of levels, and the level index is not the protocol enum value —
drying_mode is a cross-mapping (Low→Iron, Mid→Quick, High→Store), and
soak/steam levels are offset. Callers had no way to know what a
device/programme accepts and sent blindly.

This PR
Adds a per-device capability table (roborock/data/zeo/) that
1. resolves the mode templates a device supports from model/region/feature
   bits (resolve_mode_templates / resolve_data_module / build_mode_templates);
2. validates (validate_start_params) or generates (default_start_params) a
   valid START parameter set for a (mode, program), using that device's own
   data module. Covers 70 modules / 4212 mode records.

Follow-up
The exposed level data (ZeoParamConfig.support / raw_values, ZeoTimeOptions,
ZeoModeLists) makes it possible to offer only valid choices in a frontend, so
users cannot pick an illegal combination. It would be great if the integration
maintainers could take this further and implement it end to end.
@NOisi-x
NOisi-x force-pushed the pr/zeo-program-config branch from 5d67f37 to dd1c7d1 Compare September 21, 2026 03:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant